package editor.code;

import editor.Configuration;
import static editor.code.ASMCodePanel.*;
import editor.headers.Header;
import model.Segment;
import model.caret.TextCaret;
import model.code.*;
import model.code.line.*;
import model.symbol.*;

import java.awt.*;

/**
 * This class is responsible for rendering of any {@link Line}s.
 *
 * @author Neil Dickson
 */
public class LineRenderer {
	public static final Color	EMPTY_TEXT_COLOUR	= new Color(0xC0C0C0);
	public static final Font	EMPTY_TEXT_FONT		= Header.OTHER_FONT;

	/**
	 * Draws the Line
	 * @param line the {@link Line} to be drawn
	 * @param g the {@link Graphics} object with which to draw the line
	 * @param bounds rectangle in which to draw this line
	 */
	public static void draw(Line line,Graphics g,Rectangle bounds) {
		draw(line,g,bounds, TextCaret.NONE, TextCaret.NONE);
	}

	/**
	 * Draws the Line
	 * @param line the {@link Line} to be drawn
	 * @param g the {@link Graphics} object with which to draw the line
	 * @param bounds rectangle in which to draw this line
	 * @param caretStart start of selection on this line or {@link TextCaret#NONE} if none
	 * @param caretEnd end of selection on this line or {@link TextCaret#NONE} if none
	 */
	public static void draw(Line line,Graphics g,Rectangle bounds,int caretStart,int caretEnd) {
//		System.out.println("Clip: "+g.getClipBounds());
		if (line instanceof ASMLine) {
			draw((ASMLine)line,g,bounds,caretStart,caretEnd);
		}
		else if (line instanceof SimpleLine || line instanceof TypeLine || line instanceof ValueLine || (line instanceof NameLine && (line.getNumReferences()==0 || line.getReferences()[0]!=((NameLine)line).getParent()))) {
			drawCommon(line,g,bounds,caretStart,caretEnd);
		}
		else if (line instanceof NameLine) {
			drawNameLine((NameLine)line,g,bounds,caretStart,caretEnd,(((NameLine)line).getParent() instanceof Section) ? Header.SECTION_NAME_METRICS : Header.NAME_METRICS);
		}
		else if (line instanceof SingleLine) {
			drawCommon(line,g,bounds,caretStart,caretEnd);
		}
		else {
			throw new ClassCastException("Unknown subclass of Line: "+line.getClass());
		}
	}

	/**
	 * Draws the background of the Line (if any)
	 * @param line the {@link Line} whose background is to be drawn
	 * @param g the {@link Graphics} object with which to draw the line's background
	 * @param bounds rectangle in which to draw the background
	 */
	public static void drawBackground(Line line,Graphics g, Rectangle bounds) {
		drawBackground(line,g,bounds, TextCaret.NONE, TextCaret.NONE);
	}

	/**
	 * Draws the background of the Line (if any)
	 * @param line the {@link Line} whose background is to be drawn
	 * @param g the {@link Graphics} object with which to draw the line's background
	 * @param bounds rectangle in which to draw the background
	 * @param caretStart start of selection on this line or {@link TextCaret#NONE} if none
	 * @param caretEnd end of selection on this line or {@link TextCaret#NONE} if none
	 */
	public static void drawBackground(Line line,Graphics g, Rectangle bounds, int caretStart, int caretEnd) {
		if (line instanceof ASMLine) {
			drawBackground((ASMLine)line,g,bounds,caretStart,caretEnd);
		}
		else {
			drawBackgroundCommon(line,g,bounds,caretStart,caretEnd);
		}
	}

	/**
	 * @param line the {@link Line} in question
	 * @param column the column number for which to calculate the x-coordinate
	 * @return the x-coordinate (not including left margin) for the specified column
	 */
	public static int getX(Line line,int column) {
		if (line instanceof ASMLine || line instanceof SimpleLine || line instanceof TypeLine || line instanceof ValueLine || (line instanceof NameLine && (line.getNumReferences()==0 || line.getReferences()[0]!=((NameLine)line).getParent()))) {
			return getXFromFont(line,column,(line instanceof SimpleLine && line.getNumReferences()==0) ? Header.OTHER_METRICS : LineField.CODE_METRICS);
		}
		if (line instanceof NameLine) {
			return getXFromFont(line,column,(((NameLine)line).getParent() instanceof Section) ? Header.SECTION_NAME_METRICS : Header.NAME_METRICS);
		}
		if (line instanceof SingleLine) {
			return getXFromFont(line,column,(line.getNumReferences()==0) ? Header.OTHER_METRICS : LineField.CODE_METRICS);
		}
		throw new ClassCastException("Unknown subclass of Line: "+line.getClass());
	}

	/**
	 * @param line the {@link Line} in question
	 * @param x the x-coordinate for which to calculate the column number
	 * @return the column number of the specified x-coordinate (not including left margin)
	 */
	static int getColumn(Line line,int x) {
		if (line instanceof ASMLine || line instanceof SimpleLine || line instanceof TypeLine || line instanceof ValueLine || (line instanceof NameLine && (line.getNumReferences()==0 || line.getReferences()[0]!=((NameLine)line).getParent()))) {
			return getColumnFromFont(line,x,(line instanceof SimpleLine && line.getNumReferences()==0) ? Header.OTHER_METRICS : LineField.CODE_METRICS);
		}
		if (line instanceof NameLine) {
			return getColumnFromFont(line,x,(((NameLine)line).getParent() instanceof Section) ? Header.SECTION_NAME_METRICS : Header.NAME_METRICS);
		}
		if (line instanceof SingleLine) {
			return getColumnFromFont(line,x,Header.OTHER_METRICS);
		}
		throw new ClassCastException("Unknown subclass of Line: "+line.getClass());
	}

	/**
	 * @param line the {@link Line} in question
	 * @return the width required for the line (not including left margin)
	 */
	public static int getWidth(Line line) {
		if (line instanceof ASMLine || line instanceof SimpleLine || line instanceof TypeLine || line instanceof ValueLine || (line instanceof NameLine && (line.getNumReferences()==0 || line.getReferences()[0]!=((NameLine)line).getParent()))) {
			return getWidthFromFont(line,(line instanceof SimpleLine && line.getNumReferences()==0) ? Header.OTHER_METRICS : LineField.CODE_METRICS);
		}
		if (line instanceof NameLine) {
			return getWidthFromFont(line,(((NameLine)line).getParent() instanceof Section) ? Header.SECTION_NAME_METRICS : Header.NAME_METRICS);
		}
		if (line instanceof SingleLine) {
			return getWidthFromFont(line,Header.OTHER_METRICS);
		}
		throw new ClassCastException("Unknown subclass of Line: "+line.getClass());
	}

	public static Color getObjectColour(Object object) {
		if (object==null) {
			return ERROR_COLOUR;
		}
		if (object instanceof Directive) {
			return KEYWORD_COLOUR;
		}
		if (object instanceof Structure) {
			return STRUCTURE_COLOUR;
		}
		if (object instanceof KeywordDataType) {
			return KEYWORD_COLOUR;
		}
		if (object instanceof StructureMember) {
			return LOCAL_VAR_COLOUR;
		}
		if (object instanceof Mnemonic) {
			if (((Mnemonic)object).isJumpInstruction()) {
				return JUMP_COLOUR;
			}
			return MNEMONIC_COLOUR;
		}
		if (object instanceof Register) {
			return REGISTER_COLOUR;
		}
		if (object instanceof LineLabel) {
			return LABEL_COLOUR;
		}
		if (object instanceof Constant || object instanceof Constants) {
			return CONSTANT_COLOUR;
		}
		if (object instanceof Number) {
			return IMMEDIATE_COLOUR;
		}
		if (object instanceof GlobalVariable) {
			return GLOBAL_VAR_COLOUR;
		}
		if (object instanceof LocalVariable) {
			return LOCAL_VAR_COLOUR;
		}
		if (object instanceof Function) {
			return FUNCTION_COLOUR;
		}
		if (object instanceof Keyword) {
			return KEYWORD_COLOUR;
		}
		if (object == Line.OPERATOR_MARKER || object instanceof Section || object instanceof Segment) {
			return OPERATOR_COLOUR;
		}
		if (object == Line.STRING_LITERAL_MARKER) {
			return STRING_COLOUR;
		}
		if (object == Line.WHITE_SPACE_MARKER) {
			return null;
		}
		return ERROR_COLOUR;
	}

	public static Font getObjectFont(Object object) {
		if (object==null) {
			return ERROR_FONT;
		}
		if (object instanceof Directive) {
			return KEYWORD_FONT;
		}
		if (object instanceof Structure) {
			return STRUCTURE_FONT;
		}
		if (object instanceof KeywordDataType) {
			return KEYWORD_FONT;
		}
		if (object instanceof StructureMember) {
			return LOCAL_VAR_FONT;
		}
		if (object instanceof Mnemonic) {
			return MNEMONIC_FONT;
		}
		if (object instanceof Register) {
			return REGISTER_FONT;
		}
		if (object instanceof LineLabel) {
			return LABEL_FONT;
		}
		if (object instanceof Constant || object instanceof Constants) {
			return CONSTANT_FONT;
		}
		if (object instanceof Number) {
			return IMMEDIATE_FONT;
		}
		if (object instanceof GlobalVariable) {
			return GLOBAL_VAR_FONT;
		}
		if (object instanceof LocalVariable) {
			return LOCAL_VAR_FONT;
		}
		if (object instanceof Function) {
			return FUNCTION_FONT;
		}
		if (object instanceof Keyword) {
			return KEYWORD_FONT;
		}
		if (object == Line.OPERATOR_MARKER || object instanceof Section || object instanceof Segment) {
			return OPERATOR_FONT;
		}
		if (object == Line.WHITE_SPACE_MARKER) {
			return null;
		}
		return ERROR_FONT;
	}

	//***********************************************************************************************
	//*																								*
	//*		ASMLine-SPECIFIC FUNCTIONS																*
	//*																								*
	//***********************************************************************************************

	private static void draw(ASMLine line, Graphics g,Rectangle bounds,int caretStart,int caretEnd) {
		int baseline = bounds.y+bounds.height-g.getFont().getSize()/4;
		int fixedCaretStart = -1;
		if (caretStart<0) {
			fixedCaretStart = 0;
		}
		int fixedCol = 0;
		int col = 0;
		boolean reachedComment = false;
		for (int token=0;token<line.getNumReferences();++token) {
			if (line.getCommentStart()==token) {
				reachedComment = true;
			}
			int tokenEnd = line.getTokenColumn(token+1);
			if (line.getReferences()[token]!=Line.WHITE_SPACE_MARKER) {
				drawToken(line.toString(col,tokenEnd),line.getReferences()[token],g,bounds,fixedCol,reachedComment);
			}
			char[] text = line.getText();
			if (caretStart!=TextCaret.NONE && (fixedCaretStart < 0) && (col<=caretStart) && (tokenEnd>=caretStart)) {
				fixedCaretStart = fixedCol;
				for (int i=col;i<tokenEnd && i<caretStart;++i) {
					if (text[i]=='\t') {
						fixedCaretStart = (fixedCaretStart+4)&(~3);
					}
					else {
						++fixedCaretStart;
					}
				}
			}
			for (int i=col;i<tokenEnd;++i) {
				if (text[i]=='\t') {
					fixedCol = (fixedCol+4)&(~3);
				}
				else {
					++fixedCol;
				}
			}
			col = tokenEnd;
		}
		if (caretStart!=TextCaret.NONE) {
			String text = Line.tabsToSpaces(line.toString(caretStart,caretEnd),fixedCaretStart);
			g.setColor(SELECTION_TEXT_COLOUR);
			g.setFont(CODE_FONT);
			g.drawString(text,bounds.x+fixedCaretStart*FONT_WIDTH,baseline);
		}

		// Draw any error underlines
		SyntaxError[] errors = line.getErrors();
		if (errors!=null) {
			g.setColor(ERROR_COLOUR);
			for (int i = 0; i<line.getNumErrors(); i++) {
				SyntaxError error = errors[i];
				if (error.hasEmphasis()) {
					int x;
					int startX = getX(line, error.getStartCol());
					int endX = Math.max(startX + FONT_WIDTH, getX(line, error.getEndCol()));
					for (x = startX; x<endX - 4; x += 4) {
						g.drawLine(bounds.x + x, baseline + 2, bounds.x + x + 2, baseline);
						g.drawLine(bounds.x + x + 2, baseline, bounds.x + x + 4, baseline + 2);
					}
					if (x<endX - 2) {
						g.drawLine(bounds.x + x, baseline + 2, bounds.x + x + 2, baseline);
					}
				}
			}
		}
	}

	private static void drawBackground(ASMLine line, Graphics g, Rectangle bounds, int caretStart, int caretEnd) {
		// NOTE: The background should already be BG_COLOUR, from in LineField.paintComponent()
//		g.setColor(BG_COLOUR);
//		g.fillRect(bounds.x, bounds.y,bounds.width,bounds.height);
		if (line.hasComment()) {
			drawCommentBg(g,bounds,getX(line,line.getTokenColumn(line.getCommentStart())));
		}
		if (caretStart!=TextCaret.NONE) {
			drawSelectionBackground(g,bounds,getX(line,caretStart),getX(line,caretEnd));
		}
	}

	private static void drawNameLine(NameLine line, Graphics g,Rectangle bounds,int caretStart,int caretEnd,FontMetrics font) {
		caretStart = Math.max(Math.min(line.length(),caretStart),0);
		caretEnd = Math.max(Math.min(line.length(),caretEnd),0);
		int baseline = bounds.y+bounds.height-g.getFont().getSize()/4;
		char[] text = line.getText();
		int length = line.length();
		Color c = ((font==Header.SECTION_NAME_METRICS) ? Configuration.SECTION_NAME_COLOUR : Configuration.NAME_COLOUR).getColour();
		Font f = (font==Header.SECTION_NAME_METRICS) ? Header.SECTION_NAME_FONT : Header.NAME_FONT;
		if (c!=null && f!=null) {
			g.setColor(c);
			g.setFont(f);
			g.drawChars(text,0,length,bounds.x,baseline);
		}
		if (caretStart!=TextCaret.NONE) {
			g.setColor(SELECTION_TEXT_COLOUR);
			g.drawChars(text,caretStart,caretEnd-caretStart,bounds.x+font.charsWidth(text,0,caretStart),baseline);
		}

		// Draw any error underlines
		SyntaxError[] errors = line.getErrors();
		if (errors!=null) {
			g.setColor(ERROR_COLOUR);
			for (int i = 0; i<line.getNumErrors(); i++) {
				SyntaxError error = errors[i];
				if (error.hasEmphasis()) {
					int x;
					int startX = getX(line, error.getStartCol());
					int endX = Math.max(startX + FONT_WIDTH, getX(line, error.getEndCol()));
					for (x = startX; x<endX - 4; x += 4) {
						g.drawLine(bounds.x + x, baseline + 2, bounds.x + x + 2, baseline);
						g.drawLine(bounds.x + x + 2, baseline, bounds.x + x + 4, baseline + 2);
					}
					if (x<endX - 2) {
						g.drawLine(bounds.x + x, baseline + 2, bounds.x + x + 2, baseline);
					}
				}
			}
		}
	}

	//***********************************************************************************************
	//*																								*
	//*		COMMON FUNCTIONS																		*
	//*																								*
	//***********************************************************************************************

	private static void drawCommon(Line line, Graphics g,Rectangle bounds,int caretStart,int caretEnd) {
		int baseline = bounds.y+bounds.height-g.getFont().getSize()/4;
		int fixedCol = 0;
		int col = 0;

		if (line.getNumReferences()>0) {
			for (int token=0;token<line.getNumReferences();++token) {
				int tokenEnd = line.getTokenColumn(token+1);
				if (line.getReferences()[token]!=Line.WHITE_SPACE_MARKER) {
					drawToken(line.toString(col,tokenEnd),line.getReferences()[token],g,bounds,fixedCol,false);
				}
				char[] text = line.getText();
				for (int i=col;i<tokenEnd;++i) {
					if (text[i]=='\t') {
						fixedCol = (fixedCol+4)&(~3);
					}
					else {
						++fixedCol;
					}
				}
				col = tokenEnd;
			}
			if (caretStart!=TextCaret.NONE && caretEnd>caretStart) {
				if (caretStart<0) {
					caretStart = 0;
				}
				if (caretEnd>line.length()) {
					caretEnd = line.length();
				}
				String beforeText = Line.tabsToSpaces(line.toString(0,caretStart),0);
				String text = Line.tabsToSpaces(line.toString(caretStart,caretEnd),beforeText.length());
				g.setColor(SELECTION_TEXT_COLOUR);
				g.setFont(CODE_FONT);
				g.drawString(text,bounds.x+g.getFontMetrics().stringWidth(beforeText),baseline);
			}
		}
		else {
			Color c = Configuration.TEXT_COLOUR.getColour();
			Font f = Header.OTHER_FONT;
			g.setColor(c);
			g.setFont(f);
			g.drawChars(line.getText(),0,line.length(),bounds.x,baseline);
			if (caretStart!=TextCaret.NONE && caretEnd>caretStart) {
				if (caretStart<0) {
					caretStart = 0;
				}
				if (caretEnd>line.length()) {
					caretEnd = line.length();
				}
				g.setColor(SELECTION_TEXT_COLOUR);
				g.drawChars(line.getText(),caretStart,caretEnd-caretStart,bounds.x+g.getFontMetrics().charsWidth(line.getText(),0,caretStart),baseline);
			}
		}

		// Draw any error underlines
		SyntaxError[] errors = line.getErrors();
		if (errors!=null) {
			g.setColor(ERROR_COLOUR);
			for (int i = 0; i<line.getNumErrors(); i++) {
				SyntaxError error = errors[i];
				if (error.hasEmphasis()) {
					int x;
					int startX = getX(line,error.getStartCol());
					int endX = Math.max(startX+FONT_WIDTH,getX(line,error.getEndCol()));
					for (x=startX;x<endX-4;x+=4) {
						g.drawLine(bounds.x+x,baseline+2,bounds.x+x+2,baseline);
						g.drawLine(bounds.x+x+2,baseline,bounds.x+x+4,baseline+2);
					}
					if (x<endX-2) {
						g.drawLine(bounds.x+x,baseline+2,bounds.x+x+2,baseline);
					}
				}
			}
		}
	}
	private static int getXCommon(Line line, int column) {
		if (column<0) {
			return 0;
		}
		if (column==Integer.MAX_VALUE) {
			return Integer.MAX_VALUE;
		}
		String text = line.toString();
		int fixedCol = 0;
		int col = 0;
		for (int i=0;i<text.length();++i) {
			if (col>=column) {
				return fixedCol*FONT_WIDTH;
			}
			if (text.charAt(i)=='\t') {
				fixedCol = (((fixedCol+4)>>>2)<<2);
			}
			else {
				++fixedCol;
			}
			++col;
		}
		return fixedCol*FONT_WIDTH;
	}

	private static int getXFromFont(Line line, int column,FontMetrics font) {
		if (column<0) {
			return 0;
		}
		if (column==Integer.MAX_VALUE) {
			return Integer.MAX_VALUE;
		}
		return font.stringWidth(Line.tabsToSpaces(line.toString(0,Math.min(column,line.length())),0));
//		return font.charsWidth(line.getText(),0,Math.min(column,line.length()));
	}

	private static int getColumnCommon(Line line, int x) {
		if (x<0) {
			return -1;
		}
		// Round to the nearest normal-sized column
		int originalFixedCol = (x+(FONT_WIDTH>>1))/FONT_WIDTH;
		String text = line.toString();
		int fixedCol = 0;
		int col = 0;
		for (int i=0;i<text.length();++i) {
			if (fixedCol>=originalFixedCol) {
				return col;
			}
			if (text.charAt(i)=='\t') {
				fixedCol = (((fixedCol+4)>>>2)<<2);
			}
			else {
				++fixedCol;
			}
			++col;
		}
		return col;
	}

	private static int getColumnFromFont(Line line, int x,FontMetrics font) {
		if (x<0) {
			return -1;
		}
		// Round to the nearest normal-sized column
		char[] text = Line.tabsToSpaces(line.toString(),0).toCharArray();
		int prevWidth = 0;
		int fixedCol = 0;
		for (int i=0;i<=line.length()-1;++i) {
			if (line.getText()[i]=='\t') {
				fixedCol = (fixedCol+4)&(~3);
			}
			else {
				++fixedCol;
			}
			int width = font.charsWidth(text,0,fixedCol);
			// If past the x-coordinate, pick the closest of this one and the last.
			if (x<width) {
				return ((width-x)>=(x-prevWidth)) ? i : i+1;
			}
			prevWidth = width;
		}
		return line.length();
	}

	private static int getWidthCommon(Line line) {
		int fixedCol = 0;
		char[] text = line.getText();
		for (int i=0;i<line.length();++i) {
			if (text[i]=='\t') {
				fixedCol = (fixedCol+4)&(~3);
			}
			else {
				++fixedCol;
			}
		}
		return fixedCol*FONT_WIDTH;
	}

	private static int getWidthFromFont(Line line,FontMetrics font) {
		return font.stringWidth(Line.tabsToSpaces(line.toString(),0));
//		return font.charsWidth(line.getText(),0,line.length());
	}

	private static void drawBackgroundCommon(Line line, Graphics g, Rectangle bounds, int caretStart, int caretEnd) {
		g.setColor(BG_COLOUR);
		g.fillRect(bounds.x, bounds.y,bounds.width,bounds.height);
		if (caretStart!=TextCaret.NONE) {
			drawSelectionBackground(g,bounds,getX(line,caretStart),getX(line,caretEnd));
		}
	}


	//***********************************************************************************************
	//*																								*
	//*		MISCELLANEOUS DRAWING FUNCTIONS															*
	//*																								*
	//***********************************************************************************************

	private static void drawToken(String tokenText,Object tokenObject, Graphics g, Rectangle bounds, int fixedCol, boolean isInComment) {
		String text = Line.tabsToSpaces(tokenText,fixedCol);
		Color c = isInComment ? COMMENT_COLOUR : getObjectColour(tokenObject);
		Font f = isInComment ? COMMENT_FONT : getObjectFont(tokenObject);
		if (c!=null && f!=null) {
			g.setColor(c);
			g.setFont(f);
			g.drawString(text,bounds.x+fixedCol*FONT_WIDTH,bounds.y+bounds.height-g.getFont().getSize()/4);
		}
	}

	public static void drawEmptyText(String text,Graphics g, Rectangle bounds) {
		g.setColor(EMPTY_TEXT_COLOUR);
		g.setFont(EMPTY_TEXT_FONT);
		g.drawString(text,bounds.x+1,bounds.y+bounds.height-g.getFont().getSize()/4);
	}

	private static void drawCommentBg(Graphics g, Rectangle bounds,int xOffset) {
		g.setColor(COMMENT_BG_COLOUR);
		g.fillRect(bounds.x+xOffset,bounds.y,bounds.width-xOffset,bounds.height);
	}
	private static void drawSelectionBackground(Graphics g, Rectangle bounds, int caretStartX, int caretEndX) {
		caretEndX = Math.min(caretEndX,bounds.width);
		g.setColor(SELECTION_BG_COLOUR);
		g.fillRect(bounds.x+caretStartX,bounds.y,caretEndX-caretStartX,bounds.height);
	}
}
